package com.boboPlanet.user.lock.impl;

import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Collections;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * @Description: TODO 分布式redis锁 非公平可重入锁
 * @Author: mis_wu
 * @Date: 2023/3/8
 * @Description: todo:ReentrantLock中的非公平可重入锁实现分布式可重入锁  hash+lua脚本
 * todo:实现思路：redis的hash数据结构: key,key,value => lockName,uuid,value
 **/
@Slf4j
public class DistributedRedisLock implements Lock {

    private final StringRedisTemplate stringRedisTemplate;

    /**
     * 锁名称,由客户端指定
     */
    private final String lockName;

    /**
     * 单例模式，每个线程唯一的uuid
     */
    private final String uuid;

    /**
     * 过期时间 单位: s/秒
     * 默认：30s
     */
    private Long expire = 30L;

    public DistributedRedisLock(StringRedisTemplate stringRedisTemplate,String lockName,String uuid){
        this.stringRedisTemplate = stringRedisTemplate;
        this.lockName = lockName;
        //uuid不能在这生成,如果同一个线程,重入的时候创建了两个对象,此处会产生两个uuid,导致产生两把锁,不是同一个锁；
        // 应当是每一个线程一个uuid,故在工厂中生成,工厂是单例的
        //this.uuid = UUID.randomUUID().toString();
        this.uuid = uuid + ":" + Thread.currentThread().getId();
    }

    @Override
    public void lock() {
        boolean lock = this.tryLock();
        log.info("redis lock result: {}",lock);
    }

    @Override
    public void lockInterruptibly() {}

    @Override
    public boolean tryLock() {
        try {
            return this.tryLock(-1L,TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            log.error("redis try lock fail,the error reason is: {}",e.getMessage());
            log.error(e.getMessage(),e);
        }
        return false;
    }

    @Override
    public boolean tryLock(long time, @NotNull TimeUnit unit) throws InterruptedException {
        if (time != -1){
            this.expire = unit.toSeconds(time);
        }
        String luaScript = "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 " +
                "then " +
                "   redis.call('hincrby',KEYS[1],ARGV[1],1) " +
                "   redis.call('expire',KEYS[1],ARGV[2]) " +
                "   return 1 " +
                "else " +
                "   return 0 " +
                "end";
        //todo:参数说明:DefaultRedisScript(脚本,返回值类型) Arrays(key集合)  uuid：hash第二个key  expire:value,使用的是stringRedisTemplate,故得转换为String
        while (Boolean.FALSE.equals(this.stringRedisTemplate.execute(new DefaultRedisScript<>(luaScript, Boolean.class), Collections.singletonList(lockName), uuid, String.valueOf(expire)))){
            //加锁失败
            TimeUnit.MILLISECONDS.sleep(50);
        }
        //加锁成功,返回之前,开启定时器自动续期
        this.renewExpire();
        return true;
    }

    @Override
    public void unlock() {
        String script = "if redis.call('hexists',KEYS[1],ARGV[1]) == 0 " +
                "then " +
                "   return nil " +
                "elseif redis.call('hincrby',KEYS[1],ARGV[1],-1) == 0 " +
                "then " +
                "   return redis.call('del',KEYS[1]) " +
                "else " +
                "   return 0 " +
                "end";
        //DefaultRedisScript返回值不能是boolean,nil 和 0都是false,区分不出是不是恶意释放锁,用long,nil返回null,1返回1,0返回0
        Long execute = this.stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Collections.singletonList(lockName), uuid);
        if (execute == null){
            throw new IllegalMonitorStateException("this lock【"+lockName+"】 doesn't belong to you!");
        } else if (execute == 0) {
            throw new IllegalMonitorStateException("this lock【"+lockName+"】release fail!");
        }

    }

    @NotNull
    @Override
    public Condition newCondition() {
        return null;
    }

    public void renewExpire(){
        String script = "if redis.call('hexists',KEYS[1],ARGV[1]) == 1 " +
                "then " +
                "   return redis.call('expire',KEYS[1],ARGV[2]) " +
                "else " +
                "   return 0 " +
                "end";
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                //todo:此处是定时器子线程,getThreadId()是子线程id，应该获取主线程id
                if (Boolean.TRUE.equals(stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Collections.singletonList(lockName), uuid, String.valueOf(expire)))){
                    renewExpire();
                }
            }
            //每过三分之一时间续期
        },this.expire * 1000 / 3);
    }

}
